<template>
  <div class='minio'>
    <div class='infoTop'>
      <div class='text'>
        已选中<b>{{ uploadFileList.length }}</b>个文件
        <template v-if='uploadFileList.length'>
          ，共<b>{{ getListSize(uploadFileList) }}</b>
        </template>
      </div>
      <div class='btngroup'>
        <el-upload
          multiple
          ref='upload'
          action=''
          :on-remove='handleRemove'
          :on-change='handleFileChange'
          :file-list='uploadFileList'
          :show-file-list='false'
          :auto-upload='false'>
          <template #trigger>
            <el-button plain type='primary'><span class='addFile'>选择文件</span></el-button>
          </template>

          <el-button type='success' @click='handleUpload' style='margin-left:15px'>点击上传</el-button>

        </el-upload>
      </div>
    </div>


    <el-upload v-if='!uploadFileList.length'
               drag
               multiple
               ref='upload'
               action=''
               :on-remove='handleRemove'
               :on-change='handleFileChange'
               :file-list='uploadFileList'
               :show-file-list='false'
               :auto-upload='false'
    >
      <el-icon class='el-icon--upload'>
        <upload-filled />
      </el-icon>
      <div class='el-upload__text'>
        拖拽到区域上传或 <em>点击上传</em><br /><span>（请上传小于300M的文件）</span>
      </div>
    </el-upload>

    <div class='table' v-else>
      <div class='row head'>
        <div class='col col1'>名称</div>
        <div class='col col2'>大小</div>
        <div class='col col3'>进度</div>
        <div class='col col4'>状态</div>
        <div class='col col5'>操作</div>
      </div>

      <div class='row' v-for='(item, index) in uploadFileList' :key='index'>
        <div class='col col1'>
          <svg class='coloricon' aria-hidden='true'>
            <use :xlink:href="'#' + getFileIcon(item.name)"></use>
          </svg>
          {{ giveEllipsisStr(item.name, 25) }}
        </div>
        <div class='col col2'>{{ filterSize(item.size) }}</div>
        <div class='col col3'>
          <el-progress :percentage='item.uploadProgress' class='file-progress-value' />
        </div>
        <div class='col col4'>
          <el-tag v-if="item.status === '等待上传'" size='small' type='info'>等待上传</el-tag>
          <el-tag v-else-if="item.status === '校验MD5'" size='small' type='warning'>校验文件</el-tag>
          <el-tag v-else-if="item.status === '正在上传'" size='small'>正在上传</el-tag>
          <el-tag v-else-if="item.status === '上传成功'" size='small' type='success'>上传完成</el-tag>
          <el-tag v-else size='small' type='danger'>上传错误</el-tag>
        </div>
        <div class='col col5'>
                    <span class='del' @click='delRowUpload(index)'>
                        <i class='iconfont icon-delete-fill'></i>
                    </span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { filterSize, getFileIcon, getMinIOFilePath, getFileName, getOnlyUid, giveEllipsisStr } from '@/utils/common';
import { GetQueryValue1 } from '@/utils/minioUpload';
import { delChunk, delMinFile, getUpldUrl, chunkMerge } from '@/api/minIOupload';
import {UploadFilled} from "@element-plus/icons-vue";

import { ElMessage } from 'element-plus';
import SparkMD5 from 'spark-md5';
import {isStudent, isTeach} from '@/utils/auth';
import axios from 'axios';
import uploadStore from "@/store/modules/upload";
const store = uploadStore();


let uploadFileList = ref([]);

let currentFileIndex = 0;
const maxSize = 300 * 1024 * 1024;
const chunkSize = 10 * 1024 * 1024;
// 上传并发数
const simultaneousUploads = 3;
let uploadIdInfo = null;
const FILE_UPLOAD_ID_KEY = 'file_upload_id';
const FileStatus = {
  wait: '等待上传',
  getMd5: '校验MD5',
  uploading: '正在上传',
  success: '上传成功',
  error: '上传错误',
};


//点击上传文件
const handleUpload = () => {

  const files = uploadFileList.value;
  if (files.length === 0) {
    ElMessage.error('请先选择文件');
    return;
  }

  if(files.findIndex(fileItem=>fileItem.size < 1) > -1) return ElMessage('存在空文件，请删除')

  let bool = files.some(item => {
    return (item.status == '正在上传' || item.status == '校验MD5');
  });

  if (bool) return ElMessage.warning('队列中有正在上传的文件，请稍后~~');

  let successBool = files.every(item => {
    return item.status == '上传成功';
  });

  if (successBool) return ElMessage.warning('上传任务已全部完成，请勿重复上传~~');

  // 当前操作文件
  const currentFile = files[currentFileIndex];
  currentFile.status = FileStatus.getMd5;

  // 1. 计算MD5
  getFileMd5(currentFile.raw, async (md5) => {

    //上传到哪个目录下
    let role = 'other';
    if (isTeach()) {
      role = 'teacher';
    } else if (isStudent()) {
      role = 'student';
    } else {
      role = 'other';
    }
    currentFile.uploadFileName = getMinIOFilePath(role) + getFileName(currentFile.name);
    // 已上传
    if (checkResult.data.status === 1) {
      console.log(checkResult.data);
      
      ElMessage.success("上传成功");
      console.log('文件访问地址：' + checkResult.data.url);
      currentFile.status = FileStatus.success;
      currentFile.uploadProgress = 100;
      autoNextUpload(uploadFileList.value);
      return;
    } else if (checkResult.data.status === 2) {  // "上传中" 状态
      // 获取已上传分片列表
      let chunkUploadedList = checkResult.data.chunkUploadedList;
      currentFile.chunkUploadedList = chunkUploadedList;
    } else {   // 未上传
      console.log('未上传');
    }
    console.log('文件大小：' + currentFile.size);
    console.log('文件MD5：' + md5);


    // 3. 正在创建分片
    let fileChunks = createFileChunk(currentFile.raw, chunkSize);
    if(!fileChunks) return


    let param = {
      fileName: currentFile.uploadFileName,
      fileSize: currentFile.size,
      chunkSize: chunkSize,
      md5: md5,
      contentType: 'application/octet-stream',
      customName: currentFile.name,
      byteCount: currentFile.size,
      originalName: currentFile.uploadFileName,
    };


    // 4. 获取上传url
    let uploadIdInfoResult = await getFileUploadUrls(param);


    console.log(uploadIdInfoResult);

    uploadIdInfo = uploadIdInfoResult.data;
    saveFileUploadId(uploadIdInfoResult.data);
    let uploadUrls = uploadIdInfoResult.data.uploadUrls;
    let ossId = uploadIdInfoResult.data.ossId;
    currentFile.ossId = ossId;
    if (fileChunks.length !== uploadUrls.length) {
      ElMessage.error('文件分片上传地址获取错误');
      return;
    }

    console.log(currentFile.chunkList);
    // self.$set(currentFile, 'chunkList', [])
    fileChunks.map((chunkItem, index) => {
      currentFile.chunkList.push({
        chunkNumber: index + 1,
        chunk: chunkItem,
        uploadUrl: uploadUrls[index],
        progress: 0,
        status: '—',
      });
    });

    let tempFileChunks = [];
    currentFile.chunkList.forEach((item) => {
      tempFileChunks.push(item);
    });
    currentFile.status = FileStatus.uploading;
    // 处理分片列表，删除已上传的分片
    tempFileChunks = processUploadChunkList(tempFileChunks);

    // 5. 上传
    await uploadChunkBase(tempFileChunks);
    console.log('上传完成');


    // 6. 合并文件
    const mergeResult = await mergeFile({
      uploadId: uploadIdInfo.uploadId,
      fileName: currentFile.uploadFileName,
      md5: md5,
      fileSize: currentFile.size,
    });

    if (mergeResult.code != 200) {
      currentFile.status = FileStatus.error;
      ElMessage.error(mergeResult.error);
    } else {
      currentFile.status = FileStatus.success;
      currentFile.url = mergeResult.data.url;
      console.log(currentFile);
      ElMessage.success(`[ ${currentFile.name} ]，上传成功`);
      autoNextUpload(uploadFileList.value);
    }
  });
};


//选择上传文件方法
const handleFileChange = (file, fileList) => {
  // let nsFileList= fileList.filter(item=>{
  //     return item.size<=maxSize
  // })

  let index = fileList.findIndex(item => {
    return item.size > maxSize;
  });
  if (index >= 0) fileList.splice(index, 1);

  if (file.size > maxSize) {
    ElMessage.error('文件超过200M限制,请压缩后上传~');
    return;
  }


  uploadFileList.value = fileList;


  uploadFileList.value.forEach((item) => {
    if (item.uid == file.uid) {
      // 初始化自定义属性
      initFileProperties(item);
    }
  });

};

//新上传文件初始化
const initFileProperties = (file) => {
  file.chunkList = [];
  file.status = FileStatus.wait;
  file.progressStatus = 'warning';
  file.uploadProgress = 0;
};


//清空上传列表
const handleRemove = (file, fileList) => {
  uploadFileList.value = fileList;
};


/**
 * 分片读取文件 MD5
 */
const getFileMd5 = (file, callback) => {
  const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
  const fileReader = new FileReader();
  // 计算分片数
  const totalChunks = Math.ceil(file.size / chunkSize);
  console.log('总分片数：' + totalChunks);
  let currentChunk = 0;
  const spark = new SparkMD5.ArrayBuffer();
  loadNext();
  fileReader.onload = function(e) {
    try {
      spark.append(e.target.result);
    } catch (error) {
      console.log('获取Md5错误：' + currentChunk);
    }
    if (currentChunk < totalChunks) {
      currentChunk++;
      loadNext();
    } else {
      callback(spark.end());
    }
  };
  fileReader.onerror = function() {
    console.warn('读取Md5失败，文件读取错误');
  };

  function loadNext() {
    const start = currentChunk * chunkSize;
    const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
    // 注意这里的 fileRaw
    fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
  }
};


/**
 * 文件分片
 */
const createFileChunk = (file, size = chunkSize) => {
  if(file.size < 1) return ElMessage.warning('不可上传空文件')
  const fileChunkList = [];
  let count = 0;
  while (count < file.size) {
    fileChunkList.push({
      file: file.slice(count, count + size),
    });
    count += size;
  }
  return fileChunkList;
};


/**
 * 检查文件是否存在
 */

const getFileUploadUrls = (fileParam) => {
  return getUpldUrl(fileParam);
};


/**
 * 合并文件
 */
const mergeFile = (file) => {

  let url = `/oss/minion/upload/merge?uploadId=${file.uploadId}&fileName=${file.fileName}&md5=${file.md5}&fileSize=${file.fileSize}`;

  return new Promise((resolve, reject) => {
    chunkMerge(url).then(response => {
      let data = response;
      if (!data.success) {
        file.status = FileStatus.error;
        resolve(data);
      } else {
        file.status = FileStatus.success;
        resolve(data);
      }
    }).catch(error => {
      ElMessage.error('合并文件失败：' + error);
      file.status = FileStatus.error;
      reject();
    });

  });
};


/**
 * 上传下标偏移
 */
const autoNextUpload = (files) => {
  currentFileIndex++;
  if (currentFileIndex < files.length) {
    handleUpload();
  }
};


/**
 * 保存uploadid到缓存
 */
const saveFileUploadId = (data) => {
  localStorage.setItem(FILE_UPLOAD_ID_KEY, data);
};


/**
 * 处理即将上传的分片列表，判断是否有已上传的分片，有则从列表中删除
 */
const processUploadChunkList = (chunkList) => {
  const currentFile = uploadFileList.value[currentFileIndex];

  let chunkUploadedList = currentFile.chunkUploadedList;
  if (chunkUploadedList === undefined || chunkUploadedList === null || chunkUploadedList.length === 0) {
    return chunkList;
  }
  //
  for (let i = chunkList.length - 1; i >= 0; i--) {
    const chunkItem = chunkList[i];
    for (let j = 0; j < chunkUploadedList.length; j++) {
      if (chunkItem.chunkNumber === chunkUploadedList[j]) {
        chunkList.splice(i, 1);
        break;
      }
    }
  }
  return chunkList;
};


/**
 * 上传
 */
const uploadChunkBase = (chunkList) => {

  let successCount = 0;
  let totalChunks = chunkList.length;
  return new Promise((resolve) => {
    const handler = () => {

      if (chunkList.length) {
        const chunkItem = chunkList.shift();

        if (chunkItem.stop) {
          return;
        }

        // 直接上传二进制，不需要构造 FormData，否则上传后文件损坏
        axios.put(chunkItem.uploadUrl, chunkItem.chunk.file, {
          // 上传进度处理
          onUploadProgress: checkChunkUploadProgress(chunkItem),
          headers: {
            'Content-Type': 'application/octet-stream',
          },
        }).then(response => {

          if (response.status === 200) {
            console.log('分片：' + chunkItem.chunkNumber + ' 上传成功');
            successCount++;
            // 继续上传下一个分片
            handler();
          } else {
            console.log('上传失败：' + response.status + '，' + response.statusText);
          }
        }).catch(error => {
          if (!uploadFileList.value.length) return;
          if (chunkItem.stop) {
            return;
          }
          // 更新状态
          console.log('分片：' + chunkItem.chunkNumber + ' 上传失败，' + error);
          // throw '结束上传'
          // console.log(uploadFileList.value)
          // 重新添加到队列中
          chunkList.push(chunkItem);
          handler();
        });
      }
      if (successCount >= totalChunks) {
        resolve();
      }
    };
    // 并发
    for (let i = 0; i < simultaneousUploads; i++) {
      handler();
    }
  });
};


/**
 * 检查分片上传进度
 */
const checkChunkUploadProgress = (item) => {
  return p => {
    item.progress = parseInt(String((p.loaded / p.total) * 100));
    if (item.stop) return;
    updateChunkUploadStatus(item);
  };
};
const updateChunkUploadStatus = (item) => {
  if (!uploadFileList.value.length) return;
  let status = FileStatus.uploading;
  let progressStatus = 'normal';
  if (item.progress >= 100) {
    status = FileStatus.success;
    progressStatus = 'success';
  }
  let chunkIndex = item.chunkNumber - 1;
  let currentChunk = uploadFileList.value[currentFileIndex].chunkList[chunkIndex];
  // 修改状态
  currentChunk.status = status;
  currentChunk.progressStatus = progressStatus;
  // 更新状态
  // this.$set(this.uploadFileList.value[currentFileIndex].chunkList, chunkIndex, currentChunk)
  // 获取文件上传进度
  getCurrentFileProgress();
};

const getCurrentFileProgress = () => {
  const currentFile = uploadFileList.value[currentFileIndex];
  if (!currentFile || !currentFile.chunkList) {
    return;
  }
  const chunkList = currentFile.chunkList;
  const uploadedSize = chunkList.map((item) => item.chunk.file.size * item.progress).reduce((acc, cur) => acc + cur);
  // 计算方式：已上传大小 / 文件总大小
  let progress = parseInt((uploadedSize / currentFile.size).toFixed(2));
  currentFile.uploadProgress = progress;
  // this.$set(this.uploadFileList, currentFileIndex, currentFile)
};


//获取列表中文件大小的和
const getListSize = (list) => {
  if (list.length <= 0) {
    return '0k';
  }
  let totalSize = 0;
  list.forEach(item => {
    totalSize += item.size;
  });
  return filterSize(totalSize);
};


/**
 * 删除一条上传
 */
const delRowUpload = (index) => {
  let indexFile = uploadFileList.value[index];
  if (indexFile.status == '等待上传') {
    uploadFileList.value.splice(index, 1);

  } else if (indexFile.status == '校验MD5') {
    ElMessage.warning('校验中,请稍后');
  } else if (indexFile.status == '正在上传') {
    let url = indexFile.chunkList[0].uploadUrl;
    let uploadId = GetQueryValue1('uploadId', url);

    indexFile.chunkList.map(item => {
      return item.stop = true;
    });
    uploadFileList.value.splice(index, 1);
    delChunkFun([uploadId]).then(res => {
      if (uploadFileList.value.length) {
        handleUpload();
      }
    });


  } else if (indexFile.status == '上传成功') {

    uploadFileList.value.splice(index, 1);
    currentFileIndex--;
    delMinFileFun([indexFile.ossId]).then(res => {
      console.log(res);
    });
  }

};


/**
 * 删除已上传的文件
 */
const delMinFileFun = (arr) => {
  return new Promise((resolve, reject) => {
    delMinFile({
      keys: arr,
    }).then(res => {
      resolve(res);
    }).catch(err => {
      reject(err);
    });
  });
};

/**
 * 删除分片
 */
const delChunkFun = (arr) => {
  return new Promise((resolve, reject) => {
    delChunk({
      keys: arr,
    }).then(res => {
      resolve(res);
    }).catch(err => {
      reject(err);
    });
  });
};


watch(() => uploadFileList.value, (nv) => {
  store.setUploadFileList(nv);
}, { deep: true, immediate: true });


//监听vuex上次列表的数据变化
watch(() => store.uploadFileList, (nv) => {
  uploadFileList.value = nv;
  if (nv == 0) {
    currentFileIndex = 0;
  }
});


</script>

<style lang='scss' scoped>
.minio {
  width: 100%;

  .infoTop {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 0;

    .text {
      font-size: 14px;
      color: #888;
    }
  }

  .btngroup {
    & > div {
      display: flex;
    }

    .addFile {
      padding: 0 30px;
    }
  }

  .table {
    min-height: 500px;
    margin-top: 10px;

    .row {
      display: flex;
      justify-content: space-between;
      align-items: center;
      height: 50px;

      &.head {
        background: #F6F7FB;

        .col {
          font-size: 16px;
          font-weight: 600;
        }
      }

      .col {
        padding: 0 10px;
        font-size: 14px;
        width: 20%;
        position: relative;

        .del {
          padding: 3px;
          cursor: pointer;
        }

        &.col1 {
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          flex: 1;

          .coloricon {
            font-size: 1.5em;
          }
        }

        &.col2 {
          width: 80px;
        }

        &.col3 {
          flex: 1;
        }

        &.col4 {
          width: 80px;
        }

        &.col5 {
          width: 60px;
        }
      }
    }
  }

}
</style>
